use crate::app;
use clap::{Arg, ArgAction, Command, ValueEnum};
use std::str::FromStr;
use strum_macros::{Display, EnumString};

pub const AUTH_PAM_SERVICE: &str = "auth-pam-service";
pub const AUTH_TYPE: &str = "auth-type";
pub const BIND_ADDRESS: &str = "bind-address";
pub const ENABLE_SITEMD5: &str = "enable-sitemd5";
pub const FAILED_LOGINS_POLICY: &str = "failed-logins-policy";
pub const FAILED_MAX_ATTEMPTS: &str = "failed-max-attempts";
pub const FAILED_EXPIRE_AFTER: &str = "failed-expire-after";
pub const HTTP_BIND_ADDRESS: &str = "bind-address-http";
pub const IDLE_SESSION_TIMEOUT: &str = "idle-session-timeout";
pub const INSTANCE_NAME: &str = "instance-name";
pub const LOG_LEVEL: &str = "log-level";
pub const PASSIVE_HOST: &str = "passive-host";
pub const PASSIVE_PORTS: &str = "passive-ports";
pub const PROXY_EXTERNAL_CONTROL_PORT: &str = "proxy-external-control-port";
pub const STORAGE_BACKEND_TYPE: &str = "sbe-type";
pub const ROOT_DIR: &str = "root-dir";
pub const FILE_DIR: &str = "file-dir";
pub const LOG_DIR: &str = "log-dir";
pub const LOG_KEEP_DAYS: &str = "log-keep-days";
pub const NO_DAEMON: &str = "no-daemon";
pub const PROXY: &str = "proxy";
pub const WORKERS: &str = "workers";
pub const MAX_THREADS: &str = "max-threads";
pub const THREAD_KEEPALIVE: &str = "thread-keepalive";

pub const SUB_COMMAND_USER: &str = "user";
pub const USERNAME: &str = "username";
pub const PASSWORD: &str = "password";
pub const ITER_COUNTS: &str = "iter-counts";

#[derive(Debug, EnumString, Display, PartialEq)]
#[strum(serialize_all = "lowercase")]
pub enum AuthType {
    Anonymous,
    Pam,
    Json,
}

#[derive(ValueEnum, Clone, Debug)]
#[allow(non_camel_case_types)]
pub enum StorageBackendType {
    filesystem,
}

#[derive(ValueEnum, Clone, Debug)]
#[allow(non_camel_case_types)]
pub enum FailedLoginPolicyType {
    ip,
    user,
    combination,
}

impl FromStr for FailedLoginPolicyType {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "ip" => Ok(FailedLoginPolicyType::ip),
            "user" => Ok(FailedLoginPolicyType::user),
            "combination" => Ok(FailedLoginPolicyType::combination),
            _ => Err("For --failed-logins-policy, please specify: 'ip', 'user' or 'combination'."),
        }
    }
}

#[derive(ValueEnum, Clone, Debug)]
#[allow(non_camel_case_types)]
pub enum LogLevelType {
    error,
    warn,
    info,
    debug,
    trace,
}

impl FromStr for LogLevelType {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "error" => Ok(LogLevelType::error),
            "warn" => Ok(LogLevelType::warn),
            "info" => Ok(LogLevelType::info),
            "debug" => Ok(LogLevelType::debug),
            "trace" => Ok(LogLevelType::trace),
            _ => Err("no match"),
        }
    }
}

pub(crate) fn clap_app() -> Command {
    Command::new(app::NAME)
        .subcommand_negates_reqs(true)
        .version(app::VERSION)
        .override_usage("nftp --root-dir <PATH>")
        .long_version(app::long_version())
        .about("The FTP server with notification when file is uploaded")
        .author("miao")
        .subcommand(
            Command::new(SUB_COMMAND_USER)
                .about("Encrypt password with key and salt.")
                .override_usage("nftp user -u <USERNAME> -p <PASSWORD>")
                .arg_required_else_help(true)
                .arg(
                    Arg::new(USERNAME)
                        .short('u')
                        .long("username")
                        .value_name("USERNAME")
                        .required(true)
                )
                .arg(
                    Arg::new(PASSWORD)
                        .short('p')
                        .long("password")
                        .value_name("PASSWORD")
                        .required(true)
                )
                .arg(
                    Arg::new(ITER_COUNTS)
                        .short('n')
                        .long("iter-counts")
                        .value_name("COUNTS")
                        .value_parser(u32::from_str)
                        .default_value("100000")
                )
        )
        .arg(
            Arg::new(LOG_LEVEL)
                .short('v')
                .long("log-level")
                .value_name("LEVEL")
                .value_parser(["error", "warn", "info", "debug", "trace", "off"])
                .help("Sets the logging level.")
                .default_value("info")
        )
        .arg(
            Arg::new(NO_DAEMON)
                .short('n')
                .long("no-daemon")
                .help("Do not daemonize the program")
                .action(ArgAction::SetTrue)
        )
        .arg(
            Arg::new(ROOT_DIR)
                .long("root-dir")
                .value_name("PATH")
                .help("NFTP root directory where log, auth and files are stored in.")
                .required(true)
        )
        .arg(
            Arg::new(BIND_ADDRESS)
                .long("bind-address")
                .value_name("HOST:PORT")
                .help("Sets the host and port to listen on for FTP(S) connections.")
                .default_value("0.0.0.0:21")
        )
        .arg(
            Arg::new(FAILED_LOGINS_POLICY)
                .long("failed-logins-policy")
                .value_name("POLICY")
                .value_parser(["ip", "user", "combination"])
                .help("Enable a policy for failed logins to deter \
                       password guessing (brute-force) attacks. When set to 'user', any login \
                       attempt for that user is temporary blocked if there were too many failed \
                       login attempts. When set to 'ip', any login attempt for any account is \
                       temporarily blocked from that client IP. When set to 'combination', only \
                       the specific combination of client IP and username will be blocked after \
                       too many failed login attempts.")
                .default_value("combination")
        )
        .arg(
            Arg::new(FAILED_MAX_ATTEMPTS)
                .long("failed-max-attempts")
                .value_name("ATTEMPTS")
                .help("Number of consecutive failed login attempts that activates the failed logins policy.")
                .requires(FAILED_LOGINS_POLICY)
                .default_value("3")
        )
        .arg(
            Arg::new(FAILED_EXPIRE_AFTER)
                .long("failed-expire-after")
                .value_name("SECONDS")
                .help("Number of seconds before a failed logins block entry expires.")
                .requires(FAILED_LOGINS_POLICY)
                .default_value("300")
        )
        .arg(
            Arg::new(HTTP_BIND_ADDRESS)
                .long("http-bind-address")
                .value_name("HOST:PORT")
                .help("Sets the host and port for the HTTP server used by prometheus metrics collection")
                .default_value("0.0.0.0:8080")
        )
        .arg(
            Arg::new(INSTANCE_NAME)
                .short('i')
                .long("instance-name")
                .value_name("NAME")
                .help("Gives a user friendly name to this instance. This is for used for example \
                          as part of the app name during logging and notify source")
                .default_value("nftp")
        )
        .arg(
            Arg::new(PASSIVE_PORTS)
                .long("passive-ports")
                .value_name("PORT_RANGE")
                .help("Sets the port range for data connections. In proxy protocol mode this \
                          resembles ports on the proxy.")
                .default_value("49152-65535"),
        )
        .arg(
            Arg::new(PASSIVE_HOST)
                .long("passive-host")
                .value_name("HOST")
                .help("Tells how nFTP determines the IP that is sent in response to PASV. \
                          Can be fixed, a DNS name or determined from the incoming control connection. \
                          Examples: 'from-connection', '127.0.0.1' or 'ftp.myhost.org'"
                )
                .default_value("from-connection")
        )
        .arg(
            Arg::new(AUTH_TYPE)
                .long("auth-type")
                .value_name("NAME")
                .help("The type of authorization to use")
                .default_value("json")
                .hide(true)
        )
        .arg(
            Arg::new(AUTH_PAM_SERVICE)
                .long("auth-pam-service")
                .value_name("NAME")
                .help("The name of the PAM service")
                .hide(true)
        )
        .arg(
            Arg::new(STORAGE_BACKEND_TYPE)
                .long("sbe-type")
                .value_name("TYPE")
                .help("The type of storage backend to use.")
                .default_value("filesystem")
                .hide(true)
        )
        .arg(
            Arg::new(IDLE_SESSION_TIMEOUT)
                .long("idle-session-timeout")
                .value_name("SECONDS")
                .help("The timeout in seconds after which idle connections will be closed")
                .default_value("600")
        )
        .arg(
            Arg::new(PROXY_EXTERNAL_CONTROL_PORT)
                .long("proxy-external-control-port")
                .value_name("PORT")
                .help("This switches on proxy protocol mode and sets the external control port number exposed on the proxy.")
        )
        .arg(
            Arg::new(ENABLE_SITEMD5)
                .long("enable-sitemd5")
                .help("Enable the SITE MD5 command for authenticated users (not anonymous)")
                .action(ArgAction::SetTrue)
        )
        .arg(
            Arg::new(FILE_DIR)
                .long("file-dir")
                .value_name("PATH")
                .help("The directory where files should be stored")
        )
        .arg(
            Arg::new(LOG_DIR)
                .long("log-dir")
                .value_name("PATH")
                .help("The directory where the nftp log should be stored")
        )
        .arg(
            Arg::new(LOG_KEEP_DAYS)
                .long("log-keep-days")
                .value_name("DAYS")
                .help("How many days the logs to keep")
                .value_parser(clap::value_parser!(u32).range(1..))
                .default_value("90")
        )
        .arg(
            Arg::new(PROXY)
                .short('p')
                .long("proxy")
                .value_name("ADDRESS")
                .help("Proxy address")
        )
        .arg(
            Arg::new(WORKERS)
                .long("workers")
                .value_name("COUNT")
                .help("The number of async worker threads to use, defaults to CPU cores")
                .value_parser(clap::value_parser!(u32).range(1..))
        )
        .arg(
            Arg::new(MAX_THREADS)
                .long("max-threads")
                .value_name("COUNT")
                .help("The number of max blocking threads to use that does not include async worker threads")
                .value_parser(clap::value_parser!(u32).range(1..))
                .default_value("512")
        )
        .arg(
            Arg::new(THREAD_KEEPALIVE)
                .long("thread-keepalive")
                .value_name("SECONDS")
                .help("Sets a custom timeout for a thread in the blocking pool")
                .value_parser(clap::value_parser!(u64).range(1..))
                .default_value("120")
        )
}
